package com.example.graduation.config.shiro;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.graduation.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * ProjectName graduation
 *
 * @author xieyucan
 * <br>CreateDate 2022/2/14 11:08
 */
@Component
@Scope("prototype")
@Slf4j
public class ShiroFilter extends AuthenticatingFilter {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private ThreadLocalToken localToken;

    @Resource
    private JwtUtil jwtUtil;

    @Value("${jwt.redisExpire}")
    private int redisExpire;

    /**
     * 使用http标头中提供的凭据创建一个AuthenticationToken，供登录尝试期间使用，也就是说，
     * 我可以在这里实现自定义token生成逻辑的编写，因为我本来就是在ServletRequest中传递的就是token，对此我在这里取出来即可
     *
     * @param request
     * @param response
     * @return {@link AuthenticationToken}
     * @author xieyucan
     * CreateDate 2022/2/14 11:27
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = getToken(req);
        if (StrUtil.isBlank(token)) {
            log.info("shiro拦截器的token的为空");
            return null;
        }
        return new ShiroToken(token);
    }


    /**
     * 处理受试者被拒绝访问的请求，该请求由ServletRequest提供
     * 这个方法是验证认证或者授权是否成功，也可能是认证错误，也可能是授权错误，也能是认证授权错误
     *
     * @param request
     * @param response
     * @return {@link boolean}
     * @author xieyucan
     * CreateDate 2022/2/14 11:41
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request,
                                     ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        //清空localToken内的深拷贝内容
        localToken.clean();
        String token = getToken(req);
        if (StrUtil.isBlank(token)) {
            log.info("shiro拦截器的token的为空");
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        try {
            jwtUtil.verifier(token);
        } catch (TokenExpiredException e) {
            //在这里token无效(可能是过期了，也可能是无效token)
            if (redisTemplate.hasKey(token)) {
                log.info("客户端的token过期了");
                //当我的redis里面的token不为空时，我就把redis的token删除掉
                int userId = jwtUtil.getUserIdByToken(token);
                token = jwtUtil.createToken(userId);
                redisTemplate.opsForValue().set(token, userId + "", redisExpire, TimeUnit.DAYS);
                localToken.setToken(token);
            } else {
                log.info("服务端和客户端中的token过期了");
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已过期");
                return false;
            }
        } catch (Exception e) {
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        boolean login = executeLogin(req, resp);
        return login;
    }


    /**
     * 这个方法和上面的那个方法不一样的是，他的功能比较专一，他专门处理认证错误之后的逻辑
     *
     * @param token
     * @param e
     * @param request
     * @param response
     * @return {@link boolean}
     * @author xieyucan
     * CreateDate 2022/2/14 14:43
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        /**
         * 设置response返回值参数
         */
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        try {
            log.info("认证失败");
            resp.getWriter().print(e.getMessage());
        } catch (Exception exception) {

        }
        return false;
    }


    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 希望不被shiro执行的操作
     * RequestMethod.OPTIONS这个其实就是试探性请求
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return {@link boolean}
     * @author xieyucan
     * CreateDate 2022/2/14 14:37
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest res = (HttpServletRequest) request;
        if (res.getMethod().equals(RequestMethod.OPTIONS)) {
            return true;
        }
        return false;
    }

    /**
     * 这是一个私有的方法，在这里，我希望这个方法只能在这个类中被调用
     * 我通过调用请求头获取token，当请求头中不存在是，我通过请求参数中获取token，
     * 因为这里存在post请求和get请求，或许token的存在位置不同
     *
     * @param request
     * @return {@link String}
     * @author xieyucan
     * CreateDate 2022/2/14 11:38
     */
    private String getToken(HttpServletRequest request) {
        log.info("在请求中获取token");
        String token = request.getHeader("token");
        if (StrUtil.isBlank(token)) {
            token = request.getParameter("token");
        }
        return token;
    }
}
